import re
from datetime import datetime

from django.db.models import QuerySet

import orjson

from ...models import EventDefinition, SchemaPropertyGroupProperty
from .base import EventDefinitionGenerator


class PythonGenerator(EventDefinitionGenerator):
    def generator_version(self) -> str:
        return "1.0.0"

    def language_name(self) -> str:
        return "Python"

    def generate(
        self, event_definitions: QuerySet[EventDefinition], schema_map: dict[str, list[SchemaPropertyGroupProperty]]
    ) -> str:
        now = datetime.now().isoformat()

        capture_method_blocks: list[str] = []
        used_method_names: set[str] = set()
        datetime_import = ""

        for event_def in event_definitions:
            properties = schema_map.get(str(event_def.id), [])
            event_name = event_def.name

            if not datetime_import and any(prop.property_type == "DateTime" for prop in properties):
                datetime_import = "from datetime import datetime\n"

            capture_method = self._generate_capture_method(event_name, properties, used_method_names)
            capture_method_blocks.append(capture_method)

        capture_methods_section = "\n\n".join(capture_method_blocks) if capture_method_blocks else "    pass"

        header = f'''"""
GENERATED FILE - DO NOT EDIT

This file was auto-generated by PostHog
Generated at: {now}
Generator version: {self.generator_version()}

Provides type-safe event tracking via capture_<event_name>() methods.

USAGE
=====
    from posthog_typed import PosthogTyped

    client = PosthogTyped(
        "<ph_project_api_key>",
        host="<ph_client_api_host>",
    )

    # Full autocomplete on method name, properties, and SDK kwargs
    client.capture_downloaded_file(
        file_size_b=500,
        distinct_id="user_123",
    )

    # All other Posthog methods work as usual
    client.identify(...)
    client.flush()
    client.shutdown()
"""
{datetime_import}from typing import Any, Dict, List, Optional

from typing_extensions import Unpack

from posthog import Posthog
from posthog.args import OptionalCaptureArgs


'''

        class_definition = f'''class PosthogTyped(Posthog):
    """
    A type-safe PostHog client with per-event capture methods.

    Drop-in replacement for Posthog that provides IDE autocomplete
    and type checking via capture_<event_name>() methods.
    """

{capture_methods_section}
'''

        return header + class_definition

    def _generate_capture_method(
        self, event_name: str, properties: list[SchemaPropertyGroupProperty], used_method_names: set[str]
    ) -> str:
        """Generate a capture_<event_name>() method for the PosthogTyped class."""
        method_name = self._get_unique_name(self._to_method_name(event_name), used_method_names)
        escaped_event_name = self._escape_python_string(event_name)

        if not properties:
            return f'''    def {method_name}(
        self,
        *,
        extra_properties: Optional[Dict[str, Any]] = None,
        **kwargs: Unpack[OptionalCaptureArgs],
    ) -> Optional[str]:
        """Capture a `{escaped_event_name}` event."""
        return self.capture("{escaped_event_name}", properties=extra_properties, **kwargs)'''

        required_props = sorted([p for p in properties if p.is_required], key=lambda p: p.name)
        optional_props = sorted([p for p in properties if not p.is_required], key=lambda p: p.name)

        used_names: set[str] = set()
        param_lines: list[str] = []
        required_props_str = ""

        if required_props:
            param_lines.append("        # Required event properties")
            required_properties_build_lines: list[str] = []

            for prop in required_props:
                py_type = self._map_property_type(prop.property_type)
                param_name = self._get_unique_name(self._to_python_identifier(prop.name), used_names)
                original_name = self._escape_python_string(prop.name)
                param_lines.append(f"        {param_name}: {py_type},")
                required_properties_build_lines.append(f'"{original_name}": {param_name}')

            required_props_str = ", ".join(required_properties_build_lines)
        body_lines = [f"        properties: Dict[str, Any] = {{{required_props_str}}}"]

        if optional_props:
            param_lines.append("        # Optional event properties")
            optional_assignments: list[str] = []

            for prop in optional_props:
                py_type = self._map_property_type(prop.property_type)
                param_name = self._get_unique_name(self._to_python_identifier(prop.name), used_names)
                original_name = self._escape_python_string(prop.name)
                param_lines.append(f"        {param_name}: Optional[{py_type}] = None,")
                optional_assignments.append(
                    f"""        if {param_name} is not None:
            properties["{original_name}"] = {param_name}"""
                )

            optional_str = "\n".join(optional_assignments)
            if optional_str:
                body_lines.append(optional_str)

        # Add extra_properties parameter for untyped additional properties
        param_lines.append("        # Additional untyped properties")
        param_lines.append("        extra_properties: Optional[Dict[str, Any]] = None,")
        body_lines.append(
            """        if extra_properties is not None:
            properties.update(extra_properties)"""
        )
        body_lines.append(f'        return self.capture("{escaped_event_name}", properties=properties, **kwargs)')

        params_str = "\n".join(param_lines)
        body_str = "\n".join(body_lines)

        return f'''    def {method_name}(
        self,
        *,
{params_str}
        # SDK kwargs (distinct_id, timestamp, etc.)
        **kwargs: Unpack[OptionalCaptureArgs],
    ) -> Optional[str]:
        """Capture a `{escaped_event_name}` event with type-safe properties."""
{body_str}'''

    def _map_property_type(self, property_type: str) -> str:
        """Map PostHog property types to Python types."""
        type_map = {
            "String": "str",
            "Numeric": "float",
            "Boolean": "bool",
            "DateTime": "datetime",
            "Array": "List[Any]",
            "Object": "Dict[str, Any]",
        }
        return type_map.get(property_type, "Any")

    def _to_method_name(self, event_name: str) -> str:
        """
        Convert event name to snake_case method name.
        Examples: 'downloaded_file' -> 'capture_downloaded_file', '$pageview' -> 'capture_pageview'
        """
        parts = re.split(r"[^0-9A-Za-z]+", event_name)
        snake = "_".join(p.lower() for p in parts if p)

        if not snake:
            snake = "event"
        if snake[0].isdigit():
            snake = "event_" + snake

        return "capture_" + snake

    def _to_python_identifier(self, prop_name: str) -> str:
        """
        Convert property name to valid Python identifier.
        Examples: 'file-name' -> 'file_name', '123test' -> 'prop_123test'

        Note: It attempts to make the parameters type-safe on an best effort basis
        """
        reserved_keywords = {
            "False",
            "None",
            "True",
            "and",
            "as",
            "assert",
            "async",
            "await",
            "break",
            "class",
            "continue",
            "def",
            "del",
            "elif",
            "else",
            "except",
            "finally",
            "for",
            "from",
            "global",
            "if",
            "import",
            "in",
            "is",
            "lambda",
            "nonlocal",
            "not",
            "or",
            "pass",
            "raise",
            "return",
            "try",
            "while",
            "with",
            "yield",
        }

        identifier = re.sub(r"[^0-9A-Za-z]+", "_", prop_name)
        identifier = identifier.strip("_")
        if not identifier:
            return "value"

        if identifier[0].isdigit():
            identifier = "prop_" + identifier
        if identifier in reserved_keywords:
            identifier = identifier + "_"

        return identifier

    def _get_unique_name(self, base_name: str, used_names: set[str]) -> str:
        """
        Generate a unique name by adding numeric suffixes if there are collisions.

        Examples:
            - First usage: "file_name" → "file_name"
            - Second usage: "file_name" → "file_name_2"
            - Third usage: "file_name" → "file_name_3"
        """
        if base_name not in used_names:
            used_names.add(base_name)
            return base_name

        i = 2
        while f"{base_name}_{i}" in used_names:
            i += 1
        unique_name = f"{base_name}_{i}"
        used_names.add(unique_name)
        return unique_name

    def _escape_python_string(self, value: str) -> str:
        """Escape a string for use in Python string literals"""
        return orjson.dumps(value).decode("utf-8")[1:-1]
